﻿using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.IO.Ports;
using System.Windows;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Microsoft.Win32;
using MiniExcelLibs;
using MiniExcelLibs.Attributes;

namespace SerialPortAssistant;

public partial class MainWindowViewModel : ObservableObject
{
    #region 私有字段

    /// <summary>
    /// 计时器
    /// </summary>
    readonly Stopwatch sw = new();

    /// <summary>
    /// 串口实例
    /// </summary>
    SerialPort port;

    /// <summary>
    /// PLC点位模型集合
    /// </summary>
    List<PlcPointModel> plcPointList = null;

    /// <summary>
    /// 扫描线程
    /// </summary>
    Task scanTask;

    /// <summary>
    /// 线程令牌
    /// </summary>
    CancellationTokenSource scanToken;
    #endregion 私有字段

    #region 生成通知属性的私有字段

    /// <summary>
    /// 选中的串口号
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(
        nameof(OpenOrClosePortCommand),
        nameof(RunOrStopScanCommand),
        nameof(SendDataCommand),
        nameof(SelectedConfigPathCommand),
        nameof(RefreshPortsCommand)
    )]
    string portName;

    /// <summary>
    /// 选中的波特率
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(
        nameof(OpenOrClosePortCommand),
        nameof(RunOrStopScanCommand),
        nameof(SendDataCommand),
        nameof(SelectedConfigPathCommand),
        nameof(RefreshPortsCommand)
    )]
    int baudRate = 19200;

    /// <summary>
    /// 选中的数据位
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(
        nameof(OpenOrClosePortCommand),
        nameof(RunOrStopScanCommand),
        nameof(SendDataCommand),
        nameof(SelectedConfigPathCommand),
        nameof(RefreshPortsCommand)
    )]
    int dataBit = 8;

    /// <summary>
    /// 选中的校验位
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(
        nameof(OpenOrClosePortCommand),
        nameof(RunOrStopScanCommand),
        nameof(SendDataCommand),
        nameof(SelectedConfigPathCommand),
        nameof(RefreshPortsCommand)
    )]
    string parity = Enum.GetName(typeof(Parity), System.IO.Ports.Parity.Odd);

    /// <summary>
    /// 选中的停止位
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(
        nameof(OpenOrClosePortCommand),
        nameof(RunOrStopScanCommand),
        nameof(SendDataCommand),
        nameof(SelectedConfigPathCommand),
        nameof(RefreshPortsCommand)
    )]
    string stopBit = Enum.GetName(typeof(StopBits), System.IO.Ports.StopBits.One);

    /// <summary>
    /// 可用的串口集合
    /// </summary>
    [ObservableProperty]
    ObservableCollection<string> ports = [.. SerialPort.GetPortNames()];

    /// <summary>
    /// 可用的校验位集合
    /// </summary>
    [ObservableProperty]
    ObservableCollection<string> paritys = [.. Enum.GetNames(typeof(Parity))];

    /// <summary>
    /// 可用的停止位集合
    /// </summary>
    [ObservableProperty]
    ObservableCollection<string> stopBits = [.. Enum.GetNames(typeof(StopBits))];

    /// <summary>
    /// 可用的波特率集合
    /// </summary>
    [ObservableProperty]
    ObservableCollection<int> baudRates = [9600, 19200, 38400, 57600, 115200];

    /// <summary>
    /// 可用的数据位集合
    /// </summary>
    [ObservableProperty]
    ObservableCollection<int> dataBits = [5, 6, 7, 8];

    /// <summary>
    /// 串口是否打开
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(
        nameof(RunOrStopScanCommand),
        nameof(SendDataCommand),
        nameof(SelectedConfigPathCommand),
        nameof(RefreshPortsCommand)
    )]
    bool isPortOpen;

    /// <summary>
    /// 接受到的数据
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(ClearRecivedDataCommand))]
    string recivedData;

    /// <summary>
    /// 要发送的数据
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(ClearSentDataCommand))]
    string sentData;

    /// <summary>
    /// 选中的配置文件路径
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(nameof(RunOrStopScanCommand))]
    string configPath;

    /// <summary>
    /// 是否处于自动扫描中
    /// </summary>
    [ObservableProperty]
    [NotifyCanExecuteChangedFor(
        nameof(OpenOrClosePortCommand),
        nameof(RunOrStopScanCommand),
        nameof(SendDataCommand),
        nameof(SelectedConfigPathCommand),
        nameof(RefreshPortsCommand)
    )]
    bool isAutoScanning;
    #endregion 生成通知属性的私有字段

    #region 构造函数

    /// <summary>
    /// 构造函数
    /// </summary>
    public MainWindowViewModel() => PortName = Ports?.FirstOrDefault();
    #endregion 构造函数

    #region 命令相关的实现

    /// <summary>
    /// 打开或者关闭串口命令
    /// </summary>
    [RelayCommand(CanExecute = (nameof(CanOpenOrClosePort)))]
    private void OpenOrClosePort()
    {
        if (IsPortOpen)
        {
            //应该执行关闭串口的逻辑
            ClosePort();
        }
        else
        {
            //应该执行打开串口的逻辑
            OpenPort();
        }
    }

    /// <summary>
    /// 发送数据命令
    /// </summary>
    [RelayCommand(CanExecute = (nameof(CanSendData)))]
    private async Task SendData(CancellationToken token)
    {
        string logStr = string.Empty;
        try
        {
            port.Write(SentData); //写入数据
            logStr = $"{DateTime.Now:HH:mm:ss fffff}  发送数据：{SentData.Replace("\r\n", "")}";
            await Application.Current.Dispatcher.BeginInvoke(() => RecivedData += logStr);
            string recivedStr = await Task.Run(() => ReadData(token), token); //读取数据
            logStr = $"{DateTime.Now:HH:mm:ss fffff}收到响应的数据：{recivedStr}";
            await Application.Current.Dispatcher.BeginInvoke(() => RecivedData += logStr);
        }
        catch (Exception ex)
        {
            logStr = $"发送数据到串口并等待响应的过程中发生错误：{ex.Message}";
            await Application.Current.Dispatcher.BeginInvoke(() => RecivedData += logStr);
        }
    }

    /// <summary>
    /// 是否自动扫描
    /// </summary>
    [RelayCommand(CanExecute = (nameof(CanRunOrStopScan)))]
    private void RunOrStopScan()
    {
        if (IsAutoScanning)
        {
            StopScanning();
        }
        else
        {
            StartScanning();
        }
    }

    /// <summary>
    /// 刷新可用串口号命令
    /// </summary>
    [RelayCommand(CanExecute = (nameof(CanRefreshPorts)))]
    private void RefreshPorts() => Ports = [.. SerialPort.GetPortNames()];

    /// <summary>
    /// 清空接收区数据命令
    /// </summary>
    [RelayCommand(CanExecute = (nameof(CanClearRecivedData)))]
    private void ClearRecivedData() => RecivedData = string.Empty;

    /// <summary>
    /// 清空发送区数据命令
    /// </summary>
    [RelayCommand(CanExecute = (nameof(CanClearSentData)))]
    private void ClearSentData() => SentData = string.Empty;

    /// <summary>
    /// 选择配置文件路径
    /// </summary>
    [RelayCommand(CanExecute = (nameof(CanSelectedConfigPath)))]
    private void SelectedConfigPath()
    {
        var dialog = new OpenFileDialog
        {
            Title = "请选择PLC点位配置文件：",
            Multiselect = false,
            AddExtension = false,
            AddToRecent = false,
            CheckFileExists = true,
            CheckPathExists = true,
            DefaultDirectory = System.Environment.GetFolderPath(Environment.SpecialFolder.Desktop),
            Filter = "Excel文件(*.xls,*.xlsx)|*.xls;*.xlsx"
        };
        var result = dialog.ShowDialog();
        if (result is true)
            ConfigPath = dialog.FileName;
    }

    /// <summary>
    /// 是否允许自动扫描
    /// </summary>
    /// <returns></returns>
    private bool CanRunOrStopScan() => IsPortOpen && !string.IsNullOrEmpty(ConfigPath);

    /// <summary>
    /// 是否允许开关串口
    /// </summary>
    /// <returns></returns>
    private bool CanOpenOrClosePort() =>
        !string.IsNullOrEmpty(PortName)
        && BaudRate > 0
        && DataBit > 0
        && !string.IsNullOrEmpty(Parity)
        && StopBit != Enum.GetName(typeof(StopBits), System.IO.Ports.StopBits.None)
        && !IsAutoScanning;

    /// <summary>
    /// 是否允许发送数据命令
    /// </summary>
    /// <returns></returns>
    private bool CanSendData() => IsPortOpen && !IsAutoScanning;

    private bool CanSelectedConfigPath() => IsPortOpen && !IsAutoScanning;

    /// <summary>
    /// 是否允许刷新可用的串口
    /// </summary>
    /// <returns></returns>
    private bool CanRefreshPorts() => !IsPortOpen && !IsAutoScanning;

    /// <summary>
    /// 是否允许清空发送的数据
    /// </summary>
    /// <returns></returns>
    private bool CanClearSentData() => !string.IsNullOrEmpty(SentData);

    /// <summary>
    /// 是否允许清空接收的数据
    /// </summary>
    /// <returns></returns>
    private bool CanClearRecivedData() => !string.IsNullOrEmpty(RecivedData);

    #endregion 命令相关的实现

    #region 辅助或业务逻辑相关

    /// <summary>
    /// 打开串口
    /// </summary>
    private void OpenPort()
    {
        ClosePort();
        try
        {
            var pa = Enum.TryParse(typeof(Parity), Parity, out var p)
                ? (Parity)p
                : System.IO.Ports.Parity.Odd;
            var sp = Enum.TryParse(typeof(StopBits), Parity, out var s)
                ? (StopBits)s
                : System.IO.Ports.StopBits.One;
            port = new SerialPort(PortName, BaudRate, pa, DataBit, sp);
            port.Open();
            IsPortOpen = port.IsOpen;
        }
        catch (Exception ex)
        {
            MessageBox.Show($"打开串口时发生错误：{ex.Message}");
        }
    }

    /// <summary>
    /// 关闭串口
    /// </summary>
    private void ClosePort()
    {
        if (port is null)
            return;
        if (port.IsOpen)
        {
            port.Close();
        }
        port.Dispose();
        port = null;
        IsPortOpen = false;
    }

    /// <summary>
    /// 开始扫描
    /// </summary>
    private void StartScanning()
    {
        StopScanning();
        scanToken = new CancellationTokenSource();
        LoadPlcPoints();
        if (plcPointList.Count == 0)
            return;
        scanTask = Task.Run(() => ScanningCore(scanToken.Token));
        IsAutoScanning = true;
    }

    /// <summary>
    /// 扫描的核心逻辑
    /// </summary>
    /// <param name="token"></param>
    /// <returns></returns>
    private async void ScanningCore(CancellationToken token)
    {
        string recivedStr = string.Empty;
        string cmd = string.Empty;
        string logStr = string.Empty;
        while (!token.IsCancellationRequested)
        {
            try
            {
                Application.Current.Dispatcher.Invoke(() => RecivedData = string.Empty);
                GC.Collect();
                foreach (var item in plcPointList)
                {
                    cmd = item.GetCmdStr(); //生成指令
                    port.Write(cmd); //写入数据
                    logStr = $"{DateTime.Now:HH:mm:ss fffff}  发送数据：{cmd.Replace("\r\n", "")}";
                    await Application.Current.Dispatcher.BeginInvoke(() => RecivedData += logStr);
                    recivedStr = await Task.Run(() => ReadData(token), token); //读取数据
                    logStr = $"{DateTime.Now:HH:mm:ss fffff}收到响应的数据：{recivedStr}";
                    await Application.Current.Dispatcher.BeginInvoke(() => RecivedData += logStr);
                }
                await Task.Delay(5000, token);
            }
            catch (OperationCanceledException)
            {
                await Application.Current.Dispatcher.BeginInvoke(() => RecivedData += "已经取消自动扫描");
                return;
            }
            catch (Exception ex)
            {
                await Application.Current.Dispatcher.BeginInvoke(
                    () => RecivedData += $"串口扫描发生错误：{ex.Message}"
                );
                await Task.Delay(5000, token);
            }
        }
    }

    /// <summary>
    /// 加载PLC点位
    /// </summary>
    /// <exception cref="FileNotFoundException">文件不存在时抛出的异常信息</exception>
    private void LoadPlcPoints()
    {
        if (!File.Exists(ConfigPath))
            throw new FileNotFoundException("文件不存在", ConfigPath);
        plcPointList = MiniExcel.Query<PlcPointModel>(ConfigPath, "20BIN_直进")?.ToList();
    }

    /// <summary>
    /// 停止自动扫描
    /// </summary>
    private async void StopScanning()
    {
        if (scanToken is null)
            return;
        scanToken.Cancel();
        try
        {
            await scanTask;
        }
        catch { }
        scanTask.Dispose();
        scanToken.Dispose();
        scanTask = null;
        scanToken = null;
        IsAutoScanning = false;
    }

    /// <summary>
    /// 从串口读取数据
    /// </summary>
    /// <param name="token">控制令牌</param>
    /// <returns>读取到的字符串数据</returns>
    /// <exception cref="TimeoutException">超过时间后没有数据抛出的超时异常</exception>
    public string ReadData(CancellationToken token)
    {
        var recivedStr = string.Empty;
        sw.Restart();
        while (!token.IsCancellationRequested)
        {
            recivedStr += port?.ReadExisting();
            if (recivedStr.Contains('\r') || recivedStr.Contains('\n'))
            {
                port.DiscardOutBuffer();
                port.DiscardInBuffer();
                break;
            }
            if (sw.ElapsedMilliseconds > 5000)
            {
                port.DiscardOutBuffer();
                port.DiscardInBuffer();
                throw new TimeoutException("在规定时间:5000ms内没有数据响应");
            }
        }
        return recivedStr;
    }

    #endregion 辅助或业务逻辑相关
}

/// <summary>
/// PLC点位模型
/// </summary>
public class PlcPointModel
{
    /// <summary>
    /// PLC地址点位地址
    /// </summary>
    [ExcelColumn(Name = "地址")]
    public string Address { get; set; }

    /// <summary>
    /// 地址长度
    /// </summary>
    [ExcelColumn(Name = "长度")]
    public int Length { get; set; }

    /// <summary>
    /// 地址描述
    /// </summary>
    [ExcelColumn(Name = "描述")]
    public string Describe { get; set; }

    /// <summary>
    ///  根据PLC点位获取可以直接发送的串口通信指令
    /// </summary>
    /// <param name="isWriter">是否是写入指令</param>
    /// <returns>可以直接发送的串口通信指令</returns>
    /// <exception cref="ArgumentNullException">PLC点位为空时抛出的异常</exception>
    /// <exception cref="ArgumentException">PLC点位不符合要求是抛出的异常</exception>
    public string GetCmdStr(bool isWriter = false)
    {
        if (string.IsNullOrEmpty(Address))
            throw new ArgumentNullException(nameof(Address), "地址为空");
        switch (Address.First())
        {
            case 'D'
            or 'd':
                int len = Length > 0 ? Length : 1;
                if (!int.TryParse(Address[1..], out int laddr))
                    throw new ArgumentException(nameof(Address), "无法转换地址为Int类型");
                return $"%EE#{(isWriter ? 'W' : 'R')}D{Address}{len + laddr}**\r\n";
            case 'R'
            or 'r':
                return $"%EE#{(isWriter ? 'W' : 'R')}CSR{Address[1..].PadLeft(4, '0')}**\r\n";
            default:
                throw new ArgumentException("不支持的地址类型", nameof(Address));
        }
    }
}
